/*
 *  This file is part of Bracket Properties
 *  Copyright 2011-2013 David R. Smith
 *  All Rights Reserved
 *
 */
package asia.redact.bracket.properties.serializer;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.log4j.Logger;


/**
 * Use the java Reflection API to set values on a given instance of a class. There are a number of limits on this class's capabilities.
 * 
 * @author Dave
 *
 */
public final class MethodSetter {
	
	public final Logger log = Logger.getLogger(MethodSetter.class);
	private final Pattern setterPattern = Pattern.compile("set[A-Z][a-zA-Z0-9]+");
	
	final Class<?> clazz;
	final Object instance;
	final String identifier;
	final String valueToSet;
	
	private String listDelimiter;
	
	private Lock lock = new ReentrantLock();
	
	public MethodSetter(Class<?> clazz, Object instance, String identifier, String value) {
		super();
		this.clazz = clazz;
		this.instance = instance;
		this.identifier = identifier;
		this.valueToSet = value;
		listDelimiter = " "; // space by default
	}

	/**
	 * Turn the identifier into a setter method signature, such as setName or isStopped. If the identifier already looks like a setter, return it unchanged
	 * 
	 * @return
	 */
	private String setify() {
		String str = null;
		lock.lock();
		try {
			Matcher match = setterPattern.matcher(identifier);
			if(match.matches()){
				str = identifier;
			}else{
				StringBuffer buf = new StringBuffer("set");
				char start = identifier.charAt(0);
				buf.append(Character.toUpperCase(start));
				if(identifier.length()>1) buf.append(identifier.substring(1));
				str = buf.toString();
			}
			
			return str;
			
		}finally{
			lock.unlock();
		}
	}
	
	/**
	 * Take a String representation of a parameter and turn it into the required Type to work with the setter method,
	 * based on reflection of the setter method.
	 * 
	 * Method m should be pre-screened to be the right setter
	 * 
	 * @param m
	 * @param input
	 * @return
	 */
	Object paramify(Method m, String input){
		
		if(input == null) {
			throw new RuntimeException("input cannot be null on call to paramify()");
		}
		
		if(m == null) {
			throw new RuntimeException("Method cannot be null on call to paramify()");
		}
		
		Class<?> [] cls = m.getParameterTypes();
		Class<?> argClass = cls[0];
		String name = cls[0].getName();
		
		if(argClass == String.class) return input;
		
		if(argClass.isPrimitive()) {
			switch(name){
				case "int": {
					return Integer.parseInt(input);
				}
				case "boolean": {
					return Boolean.parseBoolean(input);
				}
				case "double": {
					return Double.parseDouble(input);
				}
				case "long": {
					return Long.parseLong(input);
				}
				case "short": {
					return Short.parseShort(input);
				}
				case "float": {
					return Float.parseFloat(input);
				}
				case "char": {
					return new Character(input.charAt(0));
				}
				case "byte": {
					return Byte.valueOf(input);
				}
				case "void": {
					return null; // not sure here
				}
			}
		}
		
		if(argClass.isArray()) {
			switch(name){
				case "[C": {
					return input.toCharArray();
				}
				
			}
			
		}
		
		// we only allow the long integer Date representation. 
		if(name.equals("java.util.Date")) {
			try {
			Date date = new Date();
			date.setTime(Long.valueOf(input));
			return date;
			}catch(NumberFormatException x) {
				throw new RuntimeException("Date serialization failed - expecting a long integer, got "+valueToSet);
			}
		}
		
		// if we get this far it is not a primitive or common type like a String.  
		
		// try Enum
		if(argClass.isEnum()){
			List<?> list = Arrays.asList(argClass.getEnumConstants());
			Object targetEnumConst = null;
			for(Object obj: list){
				if(String.valueOf(obj).equals(input)){
					targetEnumConst = obj;
					break;
				}
			}
			if(targetEnumConst != null) return targetEnumConst;
		}
		
		// try one arg constructor semantics
		Object result = this.useConstructor(argClass, input);
		if(result !=null) return result;
		
		// might be a Loadable (self loading)
		if(Loadable.class.isAssignableFrom(argClass)) {

			Loadable l;
			try {
				l = (Loadable) cls[0].newInstance();
				l.fromString(input);
				return l;
			} catch (InstantiationException | IllegalAccessException e) {
				e.printStackTrace();
			}
		}
			
		// give up in disgust
		 throw new RuntimeException("Failed to figure out how to paramify "+input);
		
	}
	
	/**
	 * Sometimes in order to build the object Type to be set from a String, we can use the default constructor. However, if
	 * a Type has a one arg constructor of the correct type, we can call that.
	 * 
	 * @return
	 */
     Object useConstructor(Class<?> setterParamType, Object data){
		  Constructor<?>[] allConstructors = setterParamType.getDeclaredConstructors();
		    for (Constructor<?> ctor : allConstructors) {
			   if(ctor.getParameterTypes().length == 1) {
				   Class<?> [] typeCls = ctor.getParameterTypes();
				   if(typeCls[0] == data.getClass()){
					   // use ctor to build object
					   ctor.setAccessible(true);
				 	   try {
						return ctor.newInstance(data);
					   } catch (InstantiationException 
							| IllegalAccessException
							| IllegalArgumentException
							| InvocationTargetException e) {
						e.printStackTrace();
					   }
				   }
			   }
		    }
		    
		    return null;
	}
	
	/**
	 * Try to find the parameterized type for a method's parameter. Return null if there isn't one
	 * 
	 * @param m
	 * @return
	 */
	public String parameterizedTypeName(Method m){
		Type [] paramTypes = m.getGenericParameterTypes();
		Type paramType = paramTypes[0];
		if(paramType instanceof ParameterizedType){
			ParameterizedType pt = (ParameterizedType) paramType;
			
			Type [] actual = pt.getActualTypeArguments();
			if(actual.length==0) {
				return null;
			}
			
			Class<?> c = (Class<?>)actual[0];
			
			String parameterizedArgType = c.getName();
			log.debug("generic arg type = "+parameterizedArgType);
			return parameterizedArgType;
		}
		
		return null;
	}
	
	public void set(Object obj){
		
		lock.lock();
		
		try {
		
			// basic sanity checks
			if(identifier == null || identifier.length() == 0) {
				throw new RuntimeException("identifier cannot be null or zero length");
			}
			
			if(clazz == null || instance == null) {
				throw new RuntimeException("clazz or instance cannot be null.");
			}
			
			String preparedName = setify();
			
			Method setterMethod = null;
			
			// find the method which matches the setified identifier. We assume there is one (it is a premise of the serialization)
			Method [] allMethods = clazz.getMethods();
			Class<?> [] pType = null;
			for (Method m : allMethods) {
				if(!m.getName().equals(preparedName))  continue;
				pType =m.getParameterTypes();
				// skip any setters with multiple params, we only support one
				if(pType.length != 1) continue;
				setterMethod = m;
				break;
			}
			
			// so, should be defined by this point. 
			if(setterMethod == null) missingMethodError();
			    
			// TODO verify obj type is same as setter's expected param
			
			// fire away
			setterMethod.invoke(instance, obj);
		  	
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
			
		}finally{
			lock.unlock();
		}
	}


	public void set() throws InstantiationException, IllegalAccessException {
		
		lock.lock();
		
		try {
		
			basicSanityChecks();
			
			String preparedName = setify();
			
			Method setterMethod = null;
			
			// find the method which matches the setified identifier. We assume there is one (it is a premise of the serialization)
			Method [] allMethods = clazz.getMethods();
			Class<?> [] pType = null;
			for (Method m : allMethods) {
				if(!m.getName().equals(preparedName))  continue;
				pType =m.getParameterTypes();
				// skip any setters with multiple params, we only support one
				if(pType.length != 1) continue;
				setterMethod = m;
				break;
			}
			
			// so, should be defined by this point. 
			if(setterMethod == null) {
				log.error("!!! missing a setter method !!!!");
				log.error("class = "+clazz);
				log.error("instance being set on: "+instance);
				log.error("identifier:"+identifier);
				throw new RuntimeException("missing a setter: "+identifier+", cannot set value: "+valueToSet);
			}
			    
			// take the representation of the value and put it in the right form
			Object param = paramify(setterMethod,valueToSet);
			
			// fire away
			setterMethod.invoke(instance, param);
		  	
		} catch (Exception e) {
			e.printStackTrace();
			throw new RuntimeException(e);
			
		}finally{
			lock.unlock();
		}
	}

	public String getListDelimiter() {
		return listDelimiter;
	}

	public void setListDelimiter(String delimiter) {
		this.listDelimiter = delimiter;
	}
	
	private void missingMethodError() {
		log.error("!!! missing a setter method !!!!");
		log.error("class = "+clazz);
		log.error("instance being set on: "+instance);
		log.error("identifier:"+identifier);
	}
	
	private void basicSanityChecks() {
		// basic sanity checks
		if(identifier == null || identifier.length() == 0) {
			throw new RuntimeException("identifier cannot be null or zero length");
		}
		
		if(valueToSet == null) {
			throw new RuntimeException("valueToSet cannot be null");
		}
		
		if(clazz == null || instance == null) {
			throw new RuntimeException("clazz or instance cannot be null.");
		}
	}

}
